/* HttpAcceptor - Accepts incomming http connections.
   Copyright (C) 2003 Mark J. Wielaard

   This file is part of Snark.
   
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
 
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

package org.klomp.snark;

import java.io.*;
import java.net.*;
import java.util.*;

public class HttpAcceptor
{
  private static final String SNARKHTML
  = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
  + "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\""
  + "\"http://www.w3.org/TR/html4/loose.dtd\">"
  + "<html>"
  + "<head><title>Snark Client</title></head>"
  + "<body>"
  + "<h1>Snark Client</h1>"
  + "<p>Snark is a client for downloading and sharing files distributed with the BitTorrent protocol. It is not a normal webserver.</p>"
  + "<p><a href=\"metainfo.torrent\">Torrent</a></p>"
  + "<p><a href=\"announce\">Tracker</a></p>"
  + "<hr><p>For more info see <a href=\"http://www.klomp.org/snark/\">The Hunting of the Snark Project</a></p>"
  + "</body>"
  + "</html>";
  private static final byte[] SNARKPAGE;

  private static final String ASCII = "US-ASCII";
  private static final byte[] CRLF = new byte[] {'\r', '\n'};
  private static final byte[] HTTP_STATUS;
  private static final byte[] OK_STATUS;
  private static final byte[] CONTENT_LENGTH;
  private static final byte[] CONTENT_TYPE;

  static
  {
    try
      {
	SNARKPAGE = SNARKHTML.getBytes(ASCII);

	String STATUS = "HTTP/1.0 ";
	OK_STATUS = (STATUS + "200 OK").getBytes(ASCII);
	HTTP_STATUS = STATUS.getBytes(ASCII);
	CONTENT_LENGTH = "Content-Length: ".getBytes(ASCII);
	CONTENT_TYPE = "Content-Type: ".getBytes(ASCII);
      }
    catch (UnsupportedEncodingException uee)
      {
	// Cannot happen, US-ASCII unknown?
	throw new InternalError(uee.toString());
      }
  }

  private final Tracker tracker;

  /**
   * Creates a HttpAcceptor that can handle torrent metadata of the given
   * Tracker.
   */
  public HttpAcceptor(Tracker tracker)
  {
    this.tracker = tracker;
  }

  public void connection(Socket sock,
			 BufferedInputStream bis, BufferedOutputStream bos)
    throws IOException
  {
    BufferedReader br = new BufferedReader
      (new InputStreamReader(bis, ASCII));

    String resource = readRequest(br);
    if (Snark.debug >= Snark.INFO)
      Snark.debug("HTTP request for: " + resource, Snark.INFO);
    if (resource != null)
      {
	Map headers = readHeaders(br);
	if (Snark.debug >= Snark.DEBUG)
	  {
	    Snark.debug(headers.toString(), Snark.DEBUG);
	    Snark.debug("", Snark.DEBUG);
	  }
	
	if (resource.equals("/"))
	  sendData(bos, SNARKPAGE, "text/html");
	else if (resource.startsWith("/announce"))
	  {
	    Map params = parseParams(resource);
	    byte[] response = tracker.handleRequest(sock.getInetAddress(),
						    sock.getPort(), params);
	    sendData(bos, response, "application/octet-stream");
	  }
	else if (resource.equals("/metainfo.torrent"))
	  {
	    byte[] torrent = tracker.getMetaInfo().getTorrentData();
	    sendData(bos, torrent, "application/x-bittorrent");
	  }
	else
	  sendError(bos, 404, "Snark Client. Not a real webserver.");
      }
    else
      sendError(bos, 500, "Snark Client. Not a real webserver.");

    sock.close();
  }

  /**
   * Processes an incomming HTTP request. Only handles the most basic
   * GET requests. Returns the (URLEncoded) requested resource or null
   * if the request wasn't a valid GET request.
   */
  private static String readRequest(BufferedReader br) throws IOException
  {
    String request = br.readLine();
    if (request != null && request.startsWith("GET "))
      {
	String resource;
	int index = request.indexOf(' ', 4);
	if (index == -1)
	  resource = request.substring(4);
	else
	  resource = request.substring(4, index);
	return resource;
      }
    else
      return null;
  }

  /**
   * Consumes all headers and puts them into a Map mapping header
   * value to header key Strings.
   */
  private static Map readHeaders(BufferedReader br) throws IOException
  {
    Map m = new HashMap();
    String header = br.readLine();
    while(header != null && header.length() != 0)
      {
	header = br.readLine();
	if (header != null && header.length() != 0)
	  {
	    int index = header.indexOf(": ");
	    if (index != -1)
	      {
		String key = header.substring(0, index);
		String value = header.substring(index + 2);
		m.put(key, value);
	      }
	  }
      }
    return m;
  }

  /**
   * Sends a HTTP OK, the necessary headers and the data.
   */
  private static void sendData(OutputStream out, byte[] data,
			       String content_type)
    throws IOException
  {
    sendData(out, 200, "OK", data, content_type);
  }

  private static void sendData(OutputStream out,
			       int responseCode, String reason,
			       byte[] data, String content_type)
    throws IOException
  {
    if (Snark.debug >= Snark.DEBUG)
      Snark.debug("HTTP/1.0 " + responseCode + " " + reason + " "
		  + content_type + " (" + data.length + " bytes)",
		  Snark.DEBUG);
    byte[] type = content_type.getBytes(ASCII);
    
    // Status line
    out.write(HTTP_STATUS);
    out.write(Integer.toString(responseCode).getBytes(ASCII));
    out.write(' ');
    out.write(reason.getBytes(ASCII));
    out.write(CRLF);
    
    // Entity headers
    out.write(CONTENT_LENGTH);
    out.write(Integer.toString(data.length).getBytes(ASCII));
    out.write(CRLF);

    out.write(CONTENT_TYPE);
    out.write(type);
    out.write(CRLF);
    
    // Start of data
    out.write(CRLF);
    
    out.write(data);
    out.flush();
  }

  private static void sendError(OutputStream out,
				int responseCode, String reason)
    throws IOException
  {
    sendData(out, responseCode, reason, reason.getBytes(ASCII), "text/plain");
  }

  /**
   * Returns a key to value map of the GET request query string
   * parameters.  It expects a '?' and the urlencoded key=value
   * pairs. Note that the key and value are NOT url decoded before
   * putting in the paramaters map.
   */
  private static Map parseParams(String request)
  {
    Map m = new HashMap();
    int index = request.indexOf('?');
    if (index != -1)
      {
	String params = request.substring(index + 1);
	StringTokenizer st = new StringTokenizer(params, "&");
	while (st.hasMoreTokens())
	  {
	    String token = st.nextToken();
	    index = token.indexOf('=');
	    if (index != -1)
	      {
		String key = token.substring(0, index);
		String value = token.substring(index + 1);
		m.put(key, value);
	      }
	  }
      }
    return m;
  }
}
